/*
 * Copyright (c) 2011 Imre Fazekas.
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * Neither the name of the Brillien nor the names of its
 * terms and concepts may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package com.vii.brillien.ignition.classloading;

import com.vii.streamline.services.IOServices;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.net.*;
import java.security.*;
import java.security.cert.Certificate;
import java.util.*;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;

/**
 * Custom classloader to load all Presences published to the Brillien.
 */
public class PresenceClassLoader extends URLClassLoader {

    /**
     * logger for this class
     */
    static Logger                           _logger = LoggerFactory.getLogger(PresenceClassLoader.class);

    /**
     * list of url entries of this class loader
     */
    private List<URLEntry>                  urlSet = Collections.synchronizedList(new ArrayList());

    /**
     * cache of not found resources
     */
    private Map                             notFoundResources = new HashMap();

    /**
     * cache of not found classes
     */
    private Map                             notFoundClasses = new HashMap();

    /**
     * state flag to track whether this instance has been shut off.
     */
    private boolean                         doneCalled = false;

    /**
     * snapshot of classloader state at the time done was called
     */
    private String                          doneSnapshot;

    /**
     * streams opened by this loader
     */
    private Vector<SentinelInputStream>     streams = null;

    private ArrayList<ClassFileTransformer> transformers =
            new ArrayList<ClassFileTransformer>(1);


    private PresenceClassLoader             parentPresenceClassLoader;
    private List<PresenceClassLoader>       childrenPresenceClassLoaders;


    {
        childrenPresenceClassLoaders = Collections.synchronizedList(
                new LinkedList<PresenceClassLoader>()
        );
    }

    /**
     * Constructor.
     */
    public PresenceClassLoader() {
        super(new URL[0]);

        _logger.debug("ClassLoader: " + this + " is getting created.");
    }

    /**
     * Constructor.
     *
     * @param parent parent class loader
     */
    public PresenceClassLoader(ClassLoader parent) {
        super(new URL[0], parent);
    }

    public boolean isDone() {
        return doneCalled;
    }

    /**
     * This method should be called to free up the resources.
     * It helps garbage collection.
     */
    public void done() {

        if (doneCalled) {
            return;
        }

        if( parentPresenceClassLoader != null )
                parentPresenceClassLoader.removeChildPresenceClassLoader( this );

        // Capture the fact that the classloader is now effectively disabled.
        // First create a snapshot of our state.  This should be called
        // before setting doneCalled = true.
        doneSnapshot = "PresenceClassLoader.done() called ON " + this.toString()
                + "\n AT " + new Date();
        doneCalled = true;

        // closes the jar handles and sets the url entries to null
        int i = 0;
        while (i < this.urlSet.size()) {
            URLEntry u = (URLEntry) this.urlSet.get(i);
            if (u.zip != null) {
                try {
                    u.zip.reallyClose();
                } catch (IOException ioe) {
                    _logger.info("URLEntry closing problem at: " + u.source, ioe);
                }
            }
            if (u.table != null) {
                u.table.clear();
                u.table = null;
            }
            u = null;
            i++;
        }

        closeOpenStreams();

        // clears out the tables
        if (this.urlSet != null) {
            this.urlSet.clear();
        }
        if (this.notFoundResources != null) {
            this.notFoundResources.clear();
        }
        if (this.notFoundClasses != null) {
            this.notFoundClasses.clear();
        }

        // sets all the objects to null
        this.urlSet = null;
        this.notFoundResources = null;
        this.notFoundClasses = null;
    }


    /**
     * Adds a URL to the search list, based on the specified File.
     * <p/>
     * This variant of the method makes sure that the URL is valid, in particular
     * encoding special characters (such as blanks) in the file path.
     *
     * @param file the File to use in creating the URL
     * @throws IOException in case of errors converting the file to a URL
     */
    public synchronized void innerAppendURL(File file) throws IOException {
        try {
            appendURL(file.toURI().toURL());
        } catch (MalformedURLException mue) {
            _logger.error("loader.PresenceClassLoader_bad_url_entry", file.toURI());

            _logger.error("loader.PresenceClassLoader_malformed_url", mue);
            IOException ioe = new IOException();
            ioe.initCause(mue);
            throw ioe;
        }
    }

    public synchronized void appendURL(File file) throws IOException {
        innerAppendURL(file);

        LinkedList<File> files = new LinkedList<File>();
        IOServices.collectFiles(
                file, files, new FileFilter() {
                    public boolean accept(File file) {
                        return !file.isDirectory() && file.getName().endsWith(".jar");
                    }
                }
        );
        for (File f : files)
            innerAppendURL(f);
    }

    public PresenceClassLoader setParentPresenceClassLoader(PresenceClassLoader parentPresenceClassLoader) {
        this.parentPresenceClassLoader = parentPresenceClassLoader;
        return this;
    }

    public synchronized void addChildPresenceClassLoader(PresenceClassLoader loader) {
        childrenPresenceClassLoaders.add(loader.setParentPresenceClassLoader(this));
    }

    public synchronized void removeChildPresenceClassLoader(PresenceClassLoader loader) {
        childrenPresenceClassLoaders.remove(loader.setParentPresenceClassLoader(null));
    }

    /**
     * Appends the specified URL to the list of URLs to search for
     * classes and resources.
     *
     * @param url the URL to be added to the search path of URLs
     */
    public void addURL(URL url) {
        appendURL(url);
    }


    /**
     * Add a url to the list of urls we search for a class's bytecodes.
     *
     * @param url url to be added
     */
    public synchronized void appendURL(URL url) {

        try {
            if (url == null) {
                _logger.debug("loader.PresenceClassLoader_bad_url_entry", url);
                return;
            }

            URLEntry entry = new URLEntry(url);

            if (!urlSet.contains(entry)) {
                entry.init();
                // adds the url entry to the list
                this.urlSet.add(entry);

                if (entry.isJar) {
                    // checks the manifest if a jar
                    checkManifest(entry.zip, entry.file);
                }
            } else {
                _logger.debug("[B-CL] Ignoring duplicate URL: " + url);
                /*
                 *Clean up the unused entry or it could hold open a jar file.
                 */
                if (entry.zip != null) {
                    try {
                        entry.zip.reallyClose();
                    } catch (IOException ioe) {
                        _logger.debug("URLEntry closing problem at: " + entry.source, ioe);
                    }
                }
            }

            // clears the "not found" cache since we are adding a new url
            clearNotFoundCaches();

        } catch (IOException ioe) {

            _logger.error("loader.PresenceClassLoader_bad_url_entry", url);

            _logger.error("loader.PresenceClassLoader_malformed_url", ioe);
        }
    }

    /**
     * Returns the urls of this class loader.
     *
     * @return the urls of this class loader or an empty array
     */
    public synchronized URL[] getURLs() {

        URL[] url = null;

        if (this.urlSet != null) {
            url = new URL[this.urlSet.size()];

            for (int i = 0; i < url.length; i++) {
                url[i] = ((URLEntry) this.urlSet.get(i)).source;
            }
        } else {
            url = new URL[0];
        }

        return url;
    }

    /**
     * Returns all the "file" protocol resources of this PresenceClassLoader,
     * concatenated to a classpath string.
     * <p/>
     * Notice that this method is called by the setClassPath() method of
     * org.apache.catalina.loader.WebappLoader, since this PresenceClassLoader does
     * not addExtension off of URLClassLoader.
     *
     * @return Classpath string containing all the "file" protocol resources
     *         of this PresenceClassLoader
     */
    public String getClasspath() {

        StringBuffer strBuf = null;

        URL[] urls = getURLs();
        if (urls != null) {
            for (int i = 0; i < urls.length; i++) {
                if (urls[i].getProtocol().equals("file")) {
                    if (strBuf == null) {
                        strBuf = new StringBuffer();
                    }
                    if (i > 0) {
                        strBuf.append(File.pathSeparator);
                    }
                    strBuf.append(urls[i].getFile());
                }
            }
        }

        return (strBuf != null) ? strBuf.toString() : null;
    }

    /**
     * Refreshes the memory of the class loader.  This involves clearing the
     * not-found cahces and recreating the hash tables for the URLEntries that
     * record the files accessible for each.
     * <p/>
     * Code that creates an PresenceClassLoader and then adds files to a directory
     * that is in the loader's classpath should invoke this method after the new
     * file(s) have been added in order to update the class loader's data
     * structures which optimize class and resource searches.
     *
     * @throws IOException in case of errors refreshing the cache
     */
    public synchronized void refresh() throws IOException {
        clearNotFoundCaches();
//        for (URLEntry entry : urlSet) {
//            entry.cacheItems();
//        }
    }

    public synchronized void addTransformer(ClassFileTransformer transformer) {
        transformers.add(transformer);
    }

    /**
     * Create a new instance of a sibling classloader
     *
     * @return a new instance of a class loader that has the same visibility
     *         as this class loader
     */
    public ClassLoader copy() {
        return new DelegatingClassLoader(this);
    }

    /**
     * Erases the memory of classes and resources that have been searched for
     * but not found.
     */
    private void clearNotFoundCaches() {
        this.notFoundResources.clear();
        this.notFoundClasses.clear();
    }

    /**
     * Internal implementation of find resource.
     *
     * @param res  url resource entry
     * @param name name of the resource
     */
    private URL findResource0(final URLEntry res,
                              final String name) {

        Object result =
                AccessController.doPrivileged(new PrivilegedAction() {
                    public Object run() {

                        if (res.isJar) {

                            try {
                                JarEntry jarEntry = res.zip.getJarEntry(name);
                                if (jarEntry != null) {
                                    /*
                                    *Use a custom URL with a special stream handler to
                                    *prevent the JDK's JarURLConnection caching from
                                    *locking the jar file until JVM exit.
                                    */
                                    InternalURLStreamHandler handler = new InternalURLStreamHandler(res, name);
                                    URI uri = new URI("jar", res.source + "!/" + name, null /* fragment */);
                                    URL ret = new URL(uri.toURL(), "" /* spec */, handler);
                                    handler.tieUrl(ret);
                                    return ret;
                                }

                            } catch (Throwable thr) {
                                _logger.info("loader.excep_in_PresenceClassLoader", thr);
                            }
                        } else { // directory
                            try {
                                File resourceFile =
                                        new File(res.file.getCanonicalPath()
                                                + File.separator + name);

                                if (resourceFile.exists()) {
                                    // If we make it this far,
                                    // the resource is in the directory.
                                    return resourceFile.toURI().toURL();
                                }

                            } catch (IOException e) {
                                _logger.info("loader.excep_in_PresenceClassLoader", e);
                            }
                        }

                        return null;

                    } // End for -- each URL in classpath.
                });

        return (URL) result;
    }

    public URL findResource(String name) {

        if (doneCalled) {
            _logger.warn(
                    "FindResources already ddone" + name, this.toString(),
                    new Throwable()
            );
            return null;
        }

        // resource is in the not found list
        String nf = (String) notFoundResources.get(name);
        if (nf != null && nf.equals(name)) {
            return null;
        }

        int i = 0;
        while (i < this.urlSet.size()) {

            URLEntry u = (URLEntry) this.urlSet.get(i);

            if (!u.hasItem(name)) {
                i++;
                continue;
            }

            URL url = findResource0(u, name);
            if (url != null) return url;
            i++;
        }

        // add resource to the not found list
        notFoundResources.put(name, name);

        return null;
    }

    /**
     * Returns an enumeration of java.net.URL objects
     * representing all the resources with the given name.
     */
    public Enumeration<URL> findResources(String name) throws IOException {
        if (doneCalled) {
            _logger.warn(
                    "loader.PresenceClassLoader_done_already_called",
                    new Object[]{name, doneSnapshot}
            );
            return null;
        }
        List<URL> resourcesList = new ArrayList<URL>();

        // resource is in the not found list
        String nf = (String) notFoundResources.get(name);
        if (nf != null && nf.equals(name)) {
            return (new Vector(resourcesList)).elements();
        }


        for (Iterator iter = this.urlSet.iterator(); iter.hasNext();) {
            URLEntry urlEntry = (URLEntry) iter.next();
            URL url = findResource0(urlEntry, name);
            if (url != null) {
                resourcesList.add(url);
            }
        }

        if (resourcesList.size() == 0) {
            // add resource to the not found list
            notFoundResources.put(name, name);
        }

        return (new Vector(resourcesList)).elements();
    }


    /**
     * Checks the manifest of the given jar file.
     *
     * @param jar  the jar file that may contain manifest class path
     * @param file file pointer to the jar
     * @throws IOException if an i/o error
     */
    private void checkManifest(JarFile jar, File file) throws IOException {

        if ((jar == null) || (file == null)) return;

        Manifest man = jar.getManifest();
        if (man == null) return;

        synchronized (this) {
            String cp = man.getMainAttributes().getValue(
                    Attributes.Name.CLASS_PATH);
            if (cp == null) return;

            StringTokenizer st = new StringTokenizer(cp, " ");

            while (st.hasMoreTokens()) {
                String entry = st.nextToken();

                File newFile = new File(file.getParentFile(), entry);

                // add to class path of this class loader
                try {
                    appendURL(newFile);
                } catch (MalformedURLException ex) {
                    _logger.error("loader.PresenceClassLoader_malformed_url", ex);
                }
            }
        }
    }

    /**
     * Internal implementation of load class.
     *
     * @param res       url resource entry
     * @param entryName name of the class
     */
    private byte[] loadClassData0(final URLEntry res, final String entryName) {

        Object result =
                AccessController.doPrivileged(new PrivilegedAction() {
                    public Object run() {
                        InputStream classStream = null;
                        try {

                            if (res.isJar) { // It is a jarfile..
                                JarFile zip = res.zip;
                                JarEntry entry = zip.getJarEntry(entryName);
                                if (entry != null) {
                                    classStream = zip.getInputStream(entry);
                                    byte[] classData = getClassData(classStream);
                                    res.setProtectionDomain(PresenceClassLoader.this, entry.getCertificates());
                                    return classData;
                                }
                            } else { // Its a directory....
                                File classFile = new File(res.file,
                                        entryName.replace('/', File.separatorChar));

                                if (classFile.exists()) {
                                    try {
                                        classStream = new FileInputStream(classFile);
                                        byte[] classData = getClassData(classStream);
                                        res.setProtectionDomain(PresenceClassLoader.this, null);
                                        return classData;
                                    } finally {
                                        /*
                                        *Close the stream only if this is a directory.  The stream for
                                        *a jar/zip file was opened elsewhere and should remain open after this
                                        *method completes.
                                        */
                                        if (classStream != null) {
                                            try {
                                                classStream.close();
                                            } catch (IOException closeIOE) {
                                                _logger.info("loader.excep_in_PresenceClassLoader", closeIOE);
                                            }
                                        }
                                    }
                                }
                            }
                        } catch (IOException ioe) {
                            _logger.info("loader.excep_in_PresenceClassLoader", ioe);
                        }
                        return null;
                    }
                });
        return (byte[]) result;
    }

    protected Class findClass(String name) throws ClassNotFoundException {
        ClassData classData = findClassData(name);
        // Instruments the classes if the profiler's enabled

        // Define package information if necessary
        int lastPackageSep = name.lastIndexOf('.');
        if (lastPackageSep != -1) {
            String packageName = name.substring(0, lastPackageSep);
            if (getPackage(packageName) == null) {
                try {

                    // There's a small chance that one of our parents
                    // could define the same package after getPackage
                    // returns null but before we call definePackage,
                    // since the parent classloader instances
                    // are not locked.  So, just catch the exception
                    // that is thrown in that case and ignore it.
                    //
                    // It's unclear where we would get the info to
                    // set all spec and impl data for the package,
                    // so just use null.  This is consistent will the
                    // JDK code that does the same.
                    definePackage(packageName, null, null, null,
                            null, null, null, null);
                } catch (IllegalArgumentException iae) {
                    // duplicate attempt to define same package.
                    // safe to ignore.
                    _logger.debug("duplicate package definition attempt for " + packageName, iae);
                }
            }
        }

        // Loop though the transformers here!!
        try {
            ArrayList<ClassFileTransformer> xformers = (ArrayList<ClassFileTransformer>) transformers.clone();
            for (ClassFileTransformer transformer : xformers) {

                // see javadocs of transform().
                // It expects class name as java/lang/Object
                // as opposed to java.lang.Object
                String internalClassName = name.replace('.', '/');
                byte[] transformedBytes = transformer.transform(this, internalClassName, null,
                        classData.pd, classData.classBytes);
                if (transformedBytes != null) { // null indicates no transformation
                    _logger.info("PresenceClassLoader:findClass", "{0} actually got transformed", name);
                    classData.classBytes = transformedBytes;
                }
            }
        } catch (IllegalClassFormatException icfEx) {
            throw new ClassNotFoundException(icfEx.toString(), icfEx);
        }
        Class clazz = null;
        try {
            clazz = defineClass(name, classData.classBytes, 0, classData.classBytes.length, classData.pd);
            return clazz;
        } catch (UnsupportedClassVersionError ucve) {
            throw new UnsupportedClassVersionError(
                    "PresenceClassLoader.unsupportedVersion:" + name + " " +
                            System.getProperty("java.version")
            );
        }
    }

    /**
     * This method is responsible for locating the url from the class bytes
     * have to be read and reading the bytes. It does not actually define
     * the Class object.
     *
     * @param name class name in java.lang.Object format
     * @return class bytes as well protection domain information
     * @throws ClassNotFoundException
     */
    protected ClassData findClassData(String name) throws ClassNotFoundException {

        if (doneCalled) {
            _logger.warn(
                    "Findclass already done" + name, new Throwable()
            );
            throw new ClassNotFoundException(name);
        }

        String nf = (String) notFoundClasses.get(name);
        if (nf != null && nf.equals(name)) {
            throw new ClassNotFoundException(name);
        }

        // search thru the JARs for a file of the form java/lang/Object.class
        String entryName = name.replace('.', '/') + ".class";

        int i = 0;
        while (i < urlSet.size()) {
            URLEntry u = (URLEntry) this.urlSet.get(i);

            if (!u.hasItem(entryName)) {
                i++;
                continue;
            }

            byte[] result = loadClassData0(u, entryName);
            if (result != null) return new ClassData(result, u.pd);
            i++;
        }

        // add to the not found classes list
        notFoundClasses.put(name, name);

        throw new ClassNotFoundException(name);
    }

    /**
     * Returns the byte array from the given input stream.
     *
     * @param istream input stream to the class or resource
     * @throws IOException if an i/o error
     */
    private byte[] getClassData(InputStream istream) throws IOException {

        BufferedInputStream bstream = new BufferedInputStream(istream);
        ;
        byte[] buf = new byte[4096];
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        int num = 0;
        try {
            while ((num = bstream.read(buf)) != -1) {
                bout.write(buf, 0, num);
            }
        } finally {
            if (bstream != null) {
                try {
                    bstream.close();
                } catch (IOException closeIOE) {
                    PresenceClassLoader._logger.info("loader.excep_in_PresenceClassLoader", closeIOE);
                }
            }
        }

        return bout.toByteArray();
    }

    /**
     * Returns a string representation of this class loader.
     *
     * @return a string representation of this class loader
     */
    public String toString() {

        StringBuffer buffer = new StringBuffer();

        buffer.append("PresenceClassLoader : \n");
        if (doneCalled) {
            buffer.append("doneCalled = true" + "\n");
            if (doneSnapshot != null) {
                buffer.append("doneSnapshot = " + doneSnapshot);
            }
        } else {
            buffer.append("urlSet = " + this.urlSet + "\n");
            buffer.append("doneCalled = false " + "\n");
        }
        buffer.append(" Parent -> " + getParent() + "\n");

        return buffer.toString();
    }

    public InputStream getResourceAsStream(final String name) {
        InputStream stream = super.getResourceAsStream(name);
        /*
         *Make sure not to wrap the stream if it already is a wrapper.
         */
        if (stream != null) {
            if (!(stream instanceof SentinelInputStream)) {
                stream = new SentinelInputStream(stream);
            }
        }
        return stream;
    }


    protected synchronized Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        try{
            return super.loadClass( name, resolve );
        } catch( ClassNotFoundException cnfe ){
            for( PresenceClassLoader childLoader : childrenPresenceClassLoaders ){
                try{
                    Class c = childLoader.cascadeLoadClass( name, resolve );
                    if( c != null )
                        return c;
                } catch( ClassNotFoundException ccnfe ){ }
            }
            throw cnfe;
        }
    }

    private Class<?> cascadeLoadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        Class c = null;

        c = findLoadedClass(name);

	    if (c == null) {
            try{
                c = findClass(name);
            } catch(ClassNotFoundException e){
                for( PresenceClassLoader childLoader : childrenPresenceClassLoaders ){
                    try{
                        c = childLoader.cascadeLoadClass( name, resolve );
                        if( c != null )
                            return c;
                    } catch( ClassNotFoundException ccnfe ){ }
                }
                throw e;
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }


    /**
     * The JarFile objects loaded in the classloader may get exposed to the
     * application code (e.g. EJBs) through calls of
     * ((JarURLConnection) getResource().openConnection()).getJarFile().
     * <p/>
     * This class protects the jar file from being closed by such an application.
     *
     * @author fkieviet
     */
    private static class ProtectedJarFile extends JarFile {
        /**
         * Constructor
         *
         * @param file File
         * @throws IOException from parent
         */
        public ProtectedJarFile(File file) throws IOException {
            super(file);
        }

        /**
         * Do nothing
         *
         * @see java.util.zip.ZipFile#close()
         */
        public void close() {
            // nothing
            _logger.warn("Illegal call to close() detected", new Throwable());
        }

        /**
         * Really close the jar file
         *
         * @throws IOException from parent
         */
        public void reallyClose() throws IOException {
            super.close();
        }

        /**
         * @see java.lang.Object#finalize()
         */
        protected void finalize() throws IOException {
            reallyClose();
        }
    }

    /**
     * URL entry - keeps track of the url resources.
     */
    protected static class URLEntry {

        /**
         * the url
         */
        URL source = null;

        /**
         * file of the url
         */
        File file = null;

        /**
         * jar file if url is a jar else null
         */
        ProtectedJarFile zip = null;

        /**
         * true if url is a jar
         */
        boolean isJar = false;

        Hashtable<String, String> table = null;

        /**
         * ProtectionDomain with signers if jar is signed
         */
        ProtectionDomain pd = null;

        URLEntry(URL url) {
            source = url;
        }

        void init() throws IOException {
            try {
                file = new File(source.toURI());
                isJar = file.isFile();

                if (isJar) {
                    zip = new ProtectedJarFile(file);
                }

                table = new Hashtable<String, String>();
//                cacheItems();
            } catch (URISyntaxException use) {
                IOException ioe = new IOException();
                ioe.initCause(use);
                throw ioe;
            }
        }

        private void cacheItems() throws IOException {
            if (isJar) {
                // cache entry names from jar file
                for (Enumeration e = zip.entries(); e.hasMoreElements();) {
                    ZipEntry curEntry = (ZipEntry) e.nextElement();
                    table.put(curEntry.getName(), curEntry.getName());
                }

            } else {
                // cache entry names from directory
                if (file.exists()) {
                    fillTable(file, table, "");
                }
            }
        }

        private void fillTable(File f, Hashtable t, String parent) throws IOException {

            String localName = (parent.equals("")) ? "" : parent + "/";

            File[] children = f.listFiles();
            for (int i = 0; i < children.length; i++) {
                processFile(children[i], t, localName);
            }
        }

        /**
         * Adds a file (or, if a directory, the directory's contents) to the table
         * of files this loader knows about.
         * <p/>
         * Invokes fillTable for subdirectories which in turn invokes processFile
         * recursively.
         *
         * @param fileToProcess   the File to be processed
         * @param t               the Hashtable that holds the files the loader knows about
         * @param parentLocalName prefix to be used for the full path; should be
         *                        non-empty only for recursive invocations
         * @throws IOException in case of errors working with the fileToProcess
         */
        private void processFile(File fileToProcess, Hashtable t, String parentLocalName) throws IOException {
            String key = parentLocalName + fileToProcess.getName();
            if (fileToProcess.isFile()) {
                t.put(key, key);
            } else if (fileToProcess.isDirectory()) {
                fillTable(fileToProcess, t, key);
            }
        }


        boolean hasItem(String item) {
            // in the case of ejbc stub compilation, PresenceClassLoader is created before stubs
            // gets generated, thus we need to return true for this case.
            if (table.size() == 0) {
                return true;
            }

            /*
             *Even with the previous special handling, a file could be created
             *in a directory after the loader was created and its table of
             *URLEntry names populated.  So check the table first and, if
             *the target item is not there and this URLEntry is for a directory, look for
             *the file.  If the file is now present but was not when the loader
             *was created, add an entry for the file in the table.
             */
            boolean result = false;
            String target = item;
            // special handling
            if (item.startsWith("./")) {
                target = item.substring(2, item.length());
            }

            result = table.containsKey(target);
            if (!result && !isJar) {
                /*
                 *If the file exists now then it has been added to the directory since the
                 *loader was created.  Add it to the table of files we
                 *know about.
                 */
                File targetFile = privilegedCheckForFile(target);
                if (targetFile != null) {
                    try {
                        processFile(targetFile, table, "");
                        result = true;
                    } catch (IOException ioe) {
                        _logger.error("Error processing file:" + target + " " + file.getAbsolutePath(), ioe);
                        return false;
                    }
                }
            }
            return result;
        }

        /**
         * Returns a File object for the requested path within the URLEntry.
         * <p/>
         * Runs privileged because user code could trigger invocations of this
         * method.
         *
         * @param targetPath the relative path to look for
         * @return File object for the requested file; null if it does not exist or
         *         in case of error
         */
        private File privilegedCheckForFile(final String targetPath) {
            /*
             *Check for the file existence with privs, because this code can
             *be invoked from user code which may not otherwise have access
             *to the directories of interest.
             */
            try {
                File result = (File) AccessController.doPrivileged(new PrivilegedExceptionAction() {
                    public Object run() throws Exception {

                        File targetFile = new File(file, targetPath);
                        if (!targetFile.exists()) {
                            targetFile = null;
                        }
                        return targetFile;
                    }
                });

                return result;

            } catch (PrivilegedActionException pae) {
                /*
                 *Log any exception and return false.
                 */
                _logger.error("Error checking existence:" + targetPath + " " + file.getAbsolutePath(), pae.getCause());
                return null;
            }
        }

        /**
         * Sets ProtectionDomain with CodeSource including Signers in
         * Entry for use in call to defineClass.
         *
         * @param signers the array of signer certs or null
         */
        public void setProtectionDomain(ClassLoader PresenceClassLoader, Certificate[] signers) throws MalformedURLException {
            if (pd == null) {
                pd = new ProtectionDomain(new CodeSource(file.toURI().toURL(), signers), null, PresenceClassLoader, null);
            }
        }

        public String toString() {
            return "URLEntry : " + source.toString();
        }

        /**
         * Returns true if two URL entries has equal URLs.
         *
         * @param obj URLEntry to compare against
         * @return true if both entry has equal URL
         */
        public boolean equals(Object obj) {

            boolean tf = false;

            if (obj instanceof URLEntry) {
                URLEntry e = (URLEntry) obj;
                if (source.equals(e.source)) {
                    tf = true;
                }
            }

            return tf;
        }

        /**
         * Since equals is overridden, we need to override hashCode as well.
         */
        public int hashCode() {
            return source.hashCode();
        }

    }

    /**
     * Returns the vector of open streams; creates it if needed.
     *
     * @return Vector<SentinelInputStream> holding open streams
     */
    private Vector<SentinelInputStream> getStreams() {
        if (streams == null) {
            streams = new Vector<SentinelInputStream>();
        }
        return streams;
    }

    /**
     * Closes any streams that remain open, logging a warning for each.
     * <p/>
     * This method should be invoked when the loader will no longer be used
     * and the app will no longer explicitly close any streams it may have opened.
     */
    private void closeOpenStreams() {
        if (streams != null) {

            SentinelInputStream[] toClose = streams.toArray(new SentinelInputStream[streams.size()]);
            for (SentinelInputStream s : toClose) {
                try {
                    s.closeWithWarning();
                } catch (IOException ioe) {
                    _logger.warn("loader.PresenceClassLoader_error_closing_stream", ioe);
                }
            }
            streams.clear();
            streams = null;
        }
    }

    /**
     * Wraps all InputStreams returned by this class loader to report when
     * a finalizer is run before the stream has been closed.  This helps
     * to identify where locked files could arise.
     *
     * @author vtsyganok
     * @author tjquinn
     */
    protected class SentinelInputStream extends FilterInputStream {
        private boolean closed;// = false;
        private final Throwable throwable;

        /**
         * Constructs new FilteredInputStream which reports InputStreams not closed properly.
         * When the garbage collector runs the finalizer.  If the stream is still open this class will
         * report a stack trace showing where the stream was opened.
         *
         * @param in - InputStream to be wrapped
         */
        protected SentinelInputStream(final InputStream in) {
            super(in);
            throwable = new Throwable();
            getStreams().add(this);
        }

        /**
         * Closes underlying input stream.
         */
        public void close() throws IOException {
            _close();
        }

        /**
         * Invoked by Garbage Collector. If underlying InputStream was not closed properly,
         * the stack trace of the constructor will be logged!
         */
        protected void finalize() throws Throwable {
            if (!closed && this.in != null) {
                try {
                    in.close();
                }
                catch (IOException ignored) {
                    //Cannot do anything here.
                }
                //Well, give them a stack trace!
                report();
            }
            super.finalize();
        }

        private void _close() throws IOException {
            closed = true;
            getStreams().remove(this);
            super.close();
        }

        private void closeWithWarning() throws IOException {
            _close();
            report();
        }

        /**
         * Report "left-overs"!
         */
        private void report() {
            _logger.warn("Input stream has been finalized or forced closed without being explicitly closed; stream instantiation reported in following stack trace", this.throwable);
        }
    }

    /**
     * To properly close streams obtained through URL.getResource().getStream():
     * this opens the input stream on a JarFile that is already open as part
     * of the classloader, and returns a sentinel stream on it.
     *
     * @author fkieviet
     */
    private class InternalJarURLConnection extends JarURLConnection {
        private URL mURL;
        private URLEntry mRes;
        private String mName;

        /**
         * Constructor
         *
         * @param url  the URL that is a stream for
         * @param res  URLEntry
         * @param name String
         * @throws MalformedURLException from super class
         */
        public InternalJarURLConnection(URL url, URLEntry res, String name)
                throws MalformedURLException {
            super(url);
            mRes = res;
            mName = name;
        }

        /**
         * @see java.net.JarURLConnection#getJarFile()
         */
        public JarFile getJarFile() throws IOException {
            return mRes.zip;
        }

        /**
         * @see java.net.URLConnection#connect()
         */
        public void connect() throws IOException {
            // Nothing
        }

        /**
         * @see java.net.URLConnection#getInputStream()
         */
        public InputStream getInputStream() throws IOException {
            ZipEntry entry = mRes.zip.getEntry(mName);
            return new SentinelInputStream(mRes.zip.getInputStream(entry));
        }
    }

    /**
     * To properly close streams obtained through URL.getResource().getStream():
     * an instance of this class is instantiated for each and every URL object
     * created by this classloader. It provides a custom JarURLConnection
     * (InternalJarURLConnection) so that the stream can be obtained from an already
     * open jar file.
     *
     * @author fkieviet
     */
    private class InternalURLStreamHandler extends URLStreamHandler {
        private URL mURL;
        private URLEntry mRes;
        private String mName;

        /**
         * Constructor
         *
         * @param res  URLEntry
         * @param name String
         */
        public InternalURLStreamHandler(URLEntry res, String name) {
            mRes = res;
            mName = name;
        }

        /**
         * @see java.net.URLStreamHandler#openConnection(java.net.URL)
         */
        protected URLConnection openConnection(URL u) throws IOException {
            if (u != mURL) { // ref compare on purpose
                // This should never happen
                throw new IOException("Cannot open a foreign URL; this.url=" + mURL
                        + "; foreign.url=" + u);
            }
            return new InternalJarURLConnection(u, mRes, mName);
        }

        /**
         * Ties the URL that this handler is associated with to the handler, so
         * that it can be asserted that somehow no other URLs are mangled in (this
         * is theoretically impossible)
         *
         * @param url URL
         */
        public void tieUrl(URL url) {
            mURL = url;
        }
    }

    /**
     * This class is used as return value of findClassIntenal method  to return
     * both class bytes and protection domain.
     */
    private static class ClassData {
        protected byte[] classBytes;
        protected ProtectionDomain pd;

        ClassData(byte[] classData, ProtectionDomain pd) {
            this.classBytes = classData;
            this.pd = pd;
        }
    }

    /**
     * This class loader only provides a new class loading namespace
     * so that persistence provider can load classes in that separate
     * namespace while scanning annotations.
     * This class loader delegates all stream handling (i.e. reading
     * actual class/resource data) operations to the application class loader.
     * It only defines the Class using the byte codes.
     * Motivation behind this class is discussed at
     * https://glassfish.dev.java.net/issues/show_bug.cgi?id=237.
     */
    private static class DelegatingClassLoader extends SecureClassLoader {

        /**
         * The application class loader which is used to read class data.
         */
        private PresenceClassLoader delegate = null;

        /**
         * Create a new instance.
         *
         * @param applicationCL is the original class loader associated
         *                      with this application. The new class loader uses it to delegate
         *                      stream handling operations. The new class loader also uses
         *                      applicationCL's parent as its own parent.
         */
        DelegatingClassLoader(PresenceClassLoader applicationCL) {
            super(applicationCL.getParent()); // normal class loading delegation
            this.delegate = applicationCL;
        }

        /**
         * This method uses the delegate to use class bytes and then defines
         * the class using this class loader
         */
        protected Class findClass(String name) throws ClassNotFoundException {
            ClassData classData = delegate.findClassData(name);
            // Define package information if necessary
            int lastPackageSep = name.lastIndexOf('.');
            if (lastPackageSep != -1) {
                String packageName = name.substring(0, lastPackageSep);
                if (getPackage(packageName) == null) {
                    try {
                        // There's a small chance that one of our parents
                        // could define the same package after getPackage
                        // returns null but before we call definePackage,
                        // since the parent classloader instances
                        // are not locked.  So, just catch the exception
                        // that is thrown in that case and ignore it.
                        //
                        // It's unclear where we would get the info to
                        // set all spec and impl data for the package,
                        // so just use null.  This is consistent will the
                        // JDK code that does the same.
                        definePackage(packageName, null, null, null,
                                null, null, null, null);
                    } catch (IllegalArgumentException iae) {
                        // duplicate attempt to define same package.
                        // safe to ignore.
                        _logger.debug("duplicate package " +
                                "definition attempt for " + packageName, iae);
                    }
                }
            }
            Class clazz = null;
            try {
                clazz = defineClass(name, classData.classBytes, 0, classData.classBytes.length, classData.pd);
                return clazz;
            } catch (UnsupportedClassVersionError ucve) {
                throw new UnsupportedClassVersionError(
                        "PresenceClassLoader.unsupportedVersion" + name + " " +
                                System.getProperty("java.version")
                );
            }
        }

        protected URL findResource(String name) {
            return delegate.findResource(name);
        }

        protected Enumeration<URL> findResources(String name) throws IOException {
            return delegate.findResources(name);
        }

    }


}
